查看原文
其他

Python秘技:如何import不存在的对象

阿驹 驹说码事 2024-03-30

好久没更新了,大家久等了。平时写作时间确实较少,也并不想敷衍了事,也不想发没营养的文章,望各位海涵。《深异(中)》还有少许没有写完,所以下次再发。

今天是教大家一项 Python 秘技,如何 import 不存在的对象。源于小拳拳同学昨天在B乎上提了个问题:Python中模块变量__path__在这段代码中怎么传进来的?他想弄明白下述问题里的魔法。

问题

先有这样一个模块bluesprint_factory.py

# bluesprint_factory.py
import inspect
import sys
from flask import Blueprint

class Wrapper(object):    def __init__(self, wrapped):        self.wrapped = wrapped

   def __getattr__(self, key):        if key == 'bl':            AutoBlueprint = self.wrapped.AutoBlueprint
           return AutoBlueprint()
       return getattr(self.wrapped, key)

class AutoBlueprint(object):
   def __init__(self):        frm = sys._getframe(2)        module = inspect.getmodule(frm)        pack_name = module.__name__.rsplit('.', 1)[-1]        self.bl = Blueprint(module.__name__, pack_name)

   def __getattr__(self, key):        return getattr(self.bl, key) sys.modules[__name__] = Wrapper(sys.modules[__name__])

然后在别的模块里可以这样写:

from bluesprint_factory import bl

如此,就能成功导入一个事先并不存在的bl对象。而小拳拳自己在调试这段代码的时候发现Wrapper.__getattr__()里面首先拦截到的key值为'__path__',而不是意料中的'bl',为什么?更神奇的是,为何 return getattr(self.wrapped, key) 时,直接进入if key == 'bl':里面去了?

解析

bluesprint_factory 模块的初始化

首先看下bluesprint_factory.py做了什么事:

  1. 定义了一个Wrapper类;

  2. 定义了一个AutoBlueprint类;

  3. bluesprint_factory 模块做了一次封装并替换。

在上述第3步中,进行了如下操作:

  1. 先从 sys.modules 中得到代表当前py模块被解析后的Module对象,先称之为origin_module;

  2. origin_module对象传递给Wrapper类,得到一个Wrapper对象,称之为wrapped_module,该对象只有一个值为origin_module的属性self.wrapped;

  3. 再把wrapped_module对象绑定给 sys.modules[__name__] 这个变量。

经上述过程后:

  • sys.modules[__name__] = wrapped_module

  • wrapped_module.wrapped =  origin_module

到此为止,bluesprint_factory.py 的初始化工作就完成了。

import 语句背后的魔法

现在我们来看看从别的模块执行from bluesprint_factory import bl会发生什么:

  1. Python 解释器识别到这是一条from x import y格式的模块导入语句,会先判断x是否为 package,而判断条件就是x是否具有__path__属性,不论__path__是何值;

  2. import 机制从sys.modules中拿到bluesprint_factory模块的Module对象,即wrapped_module,然后尝试访问它的__path__属性;

  3. 由于wrapped_module对象被实例化之后并无__path__属性,于是调用了该对象的__getattr__()方法,并传递参数'__path__',企图让__getattr__()返回该属性的值(这里就是为什么__getattr__()中先拦截到了'__path__');

  4. wrapped_module.__getattr__()中第一次 if 判断显然失败,随着执行return getattr(self.wrapped, key),等价于return getattr(orign_module, '__path__');

  5. 显然 origin_module 也无该属性,返回None,此时控制权回到了 import 机制中,而且知道了bluesprint_factory就是一个正常的 module 而非 package,于是尝试从 bluesprint_factory 模块中获取bl对象;

  6. 接着需要拿到代表 bluesprint_factory.pyModule 对象,此时拿到的是 wrapped_module,如第2步,因为 sys.modules 中相应的对象已被替换;

  7. 相当于要取得 wrapped_module.bl,跟第3步同理,bl属性初始化时并不存在,于是进入wrapped_module.__getattr__(),此时的key值为'bl'

  8. 于是 if key == 'bl' 条件成立,接着调用AutoBlueprint 创建实例并返回。

弄明白了 import 机制里的流程,我们知道不仅from bluesprint_factory import bl 可以触发上述流程,import bluesprint_factory; bl = bluesprint_factory.bl 也是可以的。

这整个导入流程都是由 Python 的 import 机制控制,相关源代码参考cpython/Python/import.ccpython/Lib/importlib

总结

现在我们已经学会了如何导入一个在初始化时并不存在的对象。如果我们清楚了解 Python 的 import 机制,借助 importlib可以玩很多魔法,例如做热更新、猴子补丁、导入在运行时才能获取初始化参数的对象……

总的来说,import 机制就是 Python 解释器发现包、模块的眼睛,我们可以 hacking “眼睛” 的内部机制玩出逼真的障眼法以蒙蔽 Python 解释器。一般第三方库或框架的会采取这些魔法,让框架更为灵活强大,给使用框架的程序员带来便利,也降低框架在设计模式上的复杂度或代码量。

*END*
这里是 驹说码事,分享程序猿的码路历程
感谢您的关注

继续滑动看下一个
向上滑动看下一个

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存